"""Utility command runner."""

import argparse
import logging
import json
import os
import platform
import requests
import shutil
from subprocess import check_call
import sys
import time

from codegen import perform_codegen, lint_code, reformat_code


LOGGER = logging.getLogger(__name__)
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
NODE_ROOT = os.path.join(PROJECT_ROOT, "js")
NODE_MODULES = os.path.join(NODE_ROOT, "node_modules")
WIDGET_TARGETS = [
    os.path.join(PROJECT_ROOT, "plotly", "package_data", "widgetbundle.js"),
]

NPM_PATH = os.pathsep.join(
    [
        os.path.join(NODE_ROOT, "node_modules", ".bin"),
        os.environ.get("PATH", os.defpath),
    ]
)


def plotly_js_version():
    """Load plotly.js version from js/package.json."""

    path = os.path.join(PROJECT_ROOT, "js", "package.json")
    with open(path, "rt") as f:
        package_json = json.load(f)
        version = package_json["dependencies"]["plotly.js"]
        version = version.replace("^", "")

    return version


def install_js_deps(local):
    """Install package.json dependencies using npm."""

    npmName = "npm"
    if platform.system() == "Windows":
        npmName = "npm.cmd"

    try:
        check_call([npmName, "--version"])
        has_npm = True
    except Exception:
        has_npm = False

    skip_npm = os.environ.get("SKIP_NPM", False)
    if skip_npm:
        LOGGER.info("Skipping npm-installation")
        return

    if not has_npm:
        LOGGER.error(
            "`npm` unavailable.  If you're running this command using sudo, make sure `npm` is available to sudo"
        )

    env = os.environ.copy()
    env["PATH"] = NPM_PATH

    if has_npm:
        LOGGER.info("Installing build dependencies with npm.  This may take a while...")
        check_call(
            [npmName, "install"],
            cwd=NODE_ROOT,
            stdout=sys.stdout,
            stderr=sys.stderr,
        )
        if local is not None:
            plotly_archive = os.path.join(local, "plotly.js.tgz")
            check_call(
                [npmName, "install", plotly_archive],
                cwd=NODE_ROOT,
                stdout=sys.stdout,
                stderr=sys.stderr,
            )
        check_call(
            [npmName, "run", "build"],
            cwd=NODE_ROOT,
            stdout=sys.stdout,
            stderr=sys.stderr,
        )
        os.utime(NODE_MODULES, None)

    for target in WIDGET_TARGETS:
        if not os.path.exists(target):
            msg = "Missing file: %s" % target
            raise ValueError(msg)


def overwrite_schema_local(uri):
    """Replace plot-schema.json with local copy."""

    path = os.path.join(PROJECT_ROOT, "codegen", "resources", "plot-schema.json")
    shutil.copyfile(uri, path)


def overwrite_schema(url):
    """Replace plot-schema.json with web copy."""

    req = requests.get(url)
    assert req.status_code == 200
    path = os.path.join(PROJECT_ROOT, "codegen", "resources", "plot-schema.json")
    with open(path, "wb") as f:
        f.write(req.content)


def overwrite_bundle_local(uri):
    """Replace minified JavaScript bundle.json with local copy."""

    path = os.path.join(PROJECT_ROOT, "plotly", "package_data", "plotly.min.js")
    shutil.copyfile(uri, path)


def overwrite_bundle(url):
    """Replace minified JavaScript bundle.json with web copy."""

    req = requests.get(url)
    print("url:", url)
    assert req.status_code == 200
    path = os.path.join(PROJECT_ROOT, "plotly", "package_data", "plotly.min.js")
    with open(path, "wb") as f:
        f.write(req.content)


def overwrite_plotlyjs_version_file(plotlyjs_version):
    """Replace plotly.js version file."""

    path = os.path.join(PROJECT_ROOT, "plotly", "offline", "_plotlyjs_version.py")
    with open(path, "w") as f:
        f.write(
            """\
# DO NOT EDIT
# This file is generated by the updatebundle commands.py command
__plotlyjs_version__ = "{plotlyjs_version}"
""".format(plotlyjs_version=plotlyjs_version)
        )


def request_json(url):
    """Get JSON data from a URL."""

    req = requests.get(url)
    return json.loads(req.content.decode("utf-8"))


def get_latest_publish_build_info(repo, branch):
    """Get build info from Circle CI."""

    url = (
        r"https://circleci.com/api/v1.1/project/github/"
        r"{repo}/tree/{branch}?limit=100&filter=completed"
    ).format(repo=repo, branch=branch)

    branch_jobs = request_json(url)

    # Get most recent successful publish build for branch
    builds = [
        j
        for j in branch_jobs
        if j.get("workflows", {}).get("job_name", None) == "publish-dist"
        and j.get("status", None) == "success"
    ]
    build = builds[0]

    # Extract build info
    return {p: build[p] for p in ["vcs_revision", "build_num", "committer_date"]}


def get_bundle_schema_local(local):
    """Get paths to local build files."""

    plotly_archive = os.path.join(local, "plotly.js.tgz")
    plotly_bundle = os.path.join(local, "dist/plotly.min.js")
    plotly_schemas = os.path.join(local, "dist/plot-schema.json")
    return plotly_archive, plotly_bundle, plotly_schemas


def get_bundle_schema_urls(build_num):
    """Get URLs for required files."""

    url = (
        "https://circleci.com/api/v1.1/project/github/"
        "plotly/plotly.js/{build_num}/artifacts"
    ).format(build_num=build_num)

    artifacts = request_json(url)

    # Find archive
    archives = [a for a in artifacts if a.get("path", None) == "plotly.js.tgz"]
    archive = archives[0]

    # Find bundle
    bundles = [a for a in artifacts if a.get("path", None) == "dist/plotly.min.js"]
    bundle = bundles[0]

    # Find schema
    schemas = [a for a in artifacts if a.get("path", None) == "dist/plot-schema.json"]
    schema = schemas[0]

    return archive["url"], bundle["url"], schema["url"]


def update_schema(plotly_js_version):
    """Download latest version of the plot-schema JSON file."""

    url = (
        "https://raw.githubusercontent.com/plotly/plotly.js/"
        "v%s/dist/plot-schema.json" % plotly_js_version
    )
    overwrite_schema(url)


def update_bundle(plotly_js_version):
    """Download latest version of the plotly.js bundle."""

    url = (
        "https://raw.githubusercontent.com/plotly/plotly.js/"
        "v%s/dist/plotly.min.js" % plotly_js_version
    )
    overwrite_bundle(url)

    # Write plotly.js version file
    plotlyjs_version = plotly_js_version
    overwrite_plotlyjs_version_file(plotlyjs_version)


def update_plotlyjs(plotly_js_version, outdir):
    """Update project to a new version of plotly.js."""

    update_bundle(plotly_js_version)
    update_schema(plotly_js_version)
    perform_codegen(outdir)


# FIXME: switch to argparse
def update_schema_bundle_from_master(args):
    """Update the plotly.js schema and bundle from master."""
    if args.local is None:
        build_info = get_latest_publish_build_info(args.devrepo, args.devbranch)
        archive_url, bundle_url, schema_url = get_bundle_schema_urls(
            build_info["build_num"]
        )

        # Update bundle in package data
        overwrite_bundle(bundle_url)

        # Update schema in package data
        overwrite_schema(schema_url)
    else:
        # this info could be more informative but it doesn't seem as
        # useful in a local context and requires dependencies and
        # programming.
        build_info = {"vcs_revision": "local", "committer_date": str(time.time())}
        args.devrepo = args.local
        args.devbranch = ""

        archive_uri, bundle_uri, schema_uri = get_bundle_schema_local(args.local)
        overwrite_bundle_local(bundle_uri)
        overwrite_schema_local(schema_uri)

    # Update plotly.js url in package.json
    package_json_path = os.path.join(NODE_ROOT, "package.json")
    with open(package_json_path, "r") as f:
        package_json = json.load(f)

    # Replace version with bundle url
    package_json["dependencies"]["plotly.js"] = (
        archive_url if args.local is None else archive_uri
    )
    with open(package_json_path, "w") as f:
        json.dump(package_json, f, indent=2)

    # update plotly.js version in _plotlyjs_version
    rev = build_info["vcs_revision"]
    date = str(build_info["committer_date"])
    version = "_".join([args.devrepo, args.devbranch, date[:10], rev[:8]])
    overwrite_plotlyjs_version_file(version)
    install_js_deps(args.local)


def update_plotlyjs_dev(args, outdir):
    """Update project to a new development version of plotly.js."""

    update_schema_bundle_from_master(args)
    perform_codegen(outdir)


def parse_args():
    """Parse command-line arguments."""

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="cmd", help="Available subcommands")

    p_codegen = subparsers.add_parser("codegen", help="generate code")
    p_codegen.add_argument(
        "--noformat", action="store_true", help="prevent reformatting"
    )

    subparsers.add_parser("lint", help="lint code")

    subparsers.add_parser("format", help="reformat code")

    p_update_dev = subparsers.add_parser(
        "updateplotlyjsdev", help="update plotly.js for development"
    )
    p_update_dev.add_argument(
        "--devrepo", default="plotly/plotly.js", help="repository"
    )
    p_update_dev.add_argument("--devbranch", default="master", help="branch")
    p_update_dev.add_argument("--local", default=None, help="local path")

    subparsers.add_parser("updateplotlyjs", help="update plotly.js")

    return parser.parse_args()


def main():
    """Main driver."""

    project_root = os.path.dirname(os.path.realpath(__file__))
    outdir = os.path.join(project_root, "plotly")

    args = parse_args()

    if args.cmd == "codegen":
        perform_codegen(outdir, noformat=args.noformat)

    elif args.cmd == "format":
        reformat_code(outdir)

    elif args.cmd == "lint":
        lint_code(outdir)

    elif args.cmd == "updateplotlyjsdev":
        update_plotlyjs_dev(args, outdir)

    elif args.cmd == "updateplotlyjs":
        version = plotly_js_version()
        print(version)
        update_plotlyjs(version, outdir)

    else:
        print(f"unknown command {args.cmd}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
